%% @author Marc Worrell <marc@worrell.nl>
%% @copyright 2010-2011 Marc Worrell
%% Date: 2010-01-15
%%
%% @doc Model for managing the comments on a page.

%% Copyright 2010-2011 Marc Worrell
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.

-module(m_comment).
-moduledoc("
Accesses comments on a page.

Comments are stored in the `comment` table. Comments are no separate rsc records because that will add many extra
records and also because of access control restrictions.

When a page is not visible to a certain user then its comments shouldn’t be visible as well. To simplify this check
the comments are placed separate and made part of the rsc record.

This separate comment table also helps with cleaning up comments when the rsc record is deleted.

Todo

Finish m\\_comment
").
-author("Marc Worrell <marc@worrell.nl").

-behaviour(zotonic_model).

%% interface functions
-export([
    m_get/3,

    list_rsc/2,
    get/2,
    insert/6,
    delete/2,
    is_deletable/2,
    toggle/2,
    gravatar_code/1,
    merge/3,

    search/3
]).

-include_lib("zotonic_core/include/zotonic.hrl").

%% Cache time for comment listings and comment counts.
-define(MAXAGE_COMMENT, 7200).


%% @doc Fetch the value for the key from a model source
-spec m_get( list(), zotonic_model:opt_msg(), z:context() ) -> zotonic_model:return().
m_get([ <<"anonymous">> | Rest ], _Msg, Context) ->
    Anon = case m_config:get_value(mod_comment, anonymous, Context) of
        undefined -> true;
        V -> z_convert:to_bool(V)
    end,
    {ok, {Anon, Rest}};
m_get([ <<"moderate">> | Rest ], _Msg, Context) ->
    Mod = case m_config:get_value(mod_comment, moderate, Context) of
        undefined -> false;
        <<>> -> false;
        V -> z_convert:to_bool(V)
    end,
    {ok, {Mod, Rest}};
m_get([ <<"rsc">>, Id | Rest ], _Msg, Context) ->
    case z_acl:rsc_visible(Id, Context) of
        true -> {ok, {list_rsc(Id, Context), Rest}};
        false -> {error, eacces}
    end;
m_get([ <<"count">>, Id | Rest ], _Msg, Context) ->
    case z_acl:rsc_visible(Id, Context) of
        true -> {ok, {count_rsc(Id, Context), Rest}};
        false -> {error, eacces}
    end;
m_get([ <<"get">>, CommentId | Rest ], _Msg, Context) ->
    case get(CommentId, Context) of
        undefined ->
            {ok, {undefined, Rest}};
        Comment ->
            RscId = proplists:get_value(rsc_id, Comment),
            case z_acl:rsc_visible(RscId, Context) of
                true -> {ok, {Comment, Rest}};
                false -> {error, eacces}
            end
    end;
m_get(_Vs, _Msg, _Context) ->
    {error, unknown_path}.


%% @doc List all comments of the resource.
-spec list_rsc(m_rsc:resource(), z:context()) -> list().
list_rsc(RscId, Context) ->
    F = fun() ->
        z_db:assoc_props("select * from comment where rsc_id = $1 order by created asc", [m_rsc:rid(RscId, Context)], Context)
    end,
    z_depcache:memo(F, {comment_rsc, RscId}, ?MAXAGE_COMMENT, Context).


%% @doc Count comments of the resource.
%% @spec count_rsc(int(), Context) -> [ PropList ]
-spec count_rsc(m_rsc:resource(), z:context()) -> list().
count_rsc(RscId, Context) ->
    F = fun() ->
        z_db:q1("select count(*) from comment where rsc_id = $1", [m_rsc:rid(RscId, Context)], Context)
    end,
    z_depcache:memo(F, {comment_rsc_count, RscId}, ?MAXAGE_COMMENT, [{comment_rsc, RscId}], Context).


%% @doc Fetch a specific comment from the database.
%% @spec get(int(), Context) -> PropList
get(CommentId, Context) ->
    z_db:assoc_props_row("select * from comment where id = $1", [CommentId], Context).


%% @doc Insert a new comment. Fetches the submitter information from the Context.
-spec insert(m_rsc:resource(), Name::string(), Email::string(), Message::string(), Is_visible::boolean(), z:context()) -> {ok, pos_integer()} | {error, any()}.
insert(RscId, Name, Email, Message, Is_visible, Context) ->
    case z_acl:rsc_visible(RscId, Context)
        and (z_auth:is_auth(Context)
            orelse z_convert:to_bool(m_config:get_value(mod_comment, anonymous, true, Context))) of
        true ->
            Email = z_string:trim(Email),
            Name1 = z_html:escape(z_string:trim(Name)),
            Message1 = z_sanitize:escape_link(z_string:trim(Message), Context),
            KeepInformed = z_convert:to_bool(z_context:get_q(<<"keep_informed">>, Context, false)),
            UserAgent = z_context:get_q(<<"user_agent">>, Context, <<>>),
            IPAddress = z_convert:to_binary( m_req:get(peer, Context) ),
            {DeviceId, Context1} = case z_auth:is_auth(Context) of
                true ->
                    {undefined, Context};
                false ->
                    case m_client_local_storage:device_id(Context) of
                        {{ok, CId}, Ctx} -> {CId, Ctx};
                        {{error, _}, Ctx} -> {undefined, Ctx}
                    end
            end,
            Props = [
                {rsc_id, m_rsc:rid(RscId, Context)},
                {is_visible, Is_visible},
                {user_id, z_acl:user(Context)},
                {persistent_id, DeviceId},
                {name, Name1},
                {message, Message1},
                {email, Email},
                {gravatar_code, gravatar_code(Email)},
                {keep_informed, KeepInformed},
                {ip_address, IPAddress},
                {user_agent, UserAgent}
            ],
            {ok, CommentId} = Result = z_db:insert(comment, Props, Context1),
            z_depcache:flush({comment_rsc, RscId}, Context1),
            z_notifier:notify(#comment_insert{comment_id = CommentId, id = RscId}, Context1),
            Result;
        false ->
            {error, eacces}
    end.

%% @doc Delete a comment.  Only possible if the user has edit permission on the page.
delete(CommentId, Context) ->
    case check_editable(CommentId, Context) of
        {ok, RscId} ->
            z_db:q("delete from comment where id = $1", [CommentId], Context),
            z_depcache:flush({comment_rsc, RscId}, Context),
            ok;
        {error, _} = Error ->
            Error
    end.

%% @doc Check if a comment can be deleted
is_deletable(CommentId, Context) ->
    case check_editable(CommentId, Context) of
        {ok, _} -> true;
        {error, _} -> false
    end.

%% @doc Toggle the visibility of a comment, return the new visibility
toggle(CommentId, Context) ->
    case check_editable(CommentId, Context) of
        {ok, RscId} ->
            z_db:q("update comment
                    set is_visible = not is_visible
                    where id = $1",
                   [CommentId],
                   Context),
            z_depcache:flush({comment_rsc, RscId}, Context),
            {ok, z_db:q1("select is_visible from comment where id = $1", [CommentId], Context)};
        {error, _} = Error ->
            Error
    end.

%% @doc Check if a user can edit the comment
check_editable(CommentId, Context) ->
    case z_db:q_row("select rsc_id, user_id from comment where id = $1", [CommentId], Context) of
        {RscId, UserId} ->
            case (UserId /= undefined andalso z_acl:user(Context) == UserId)
                orelse z_acl:rsc_editable(RscId, Context)
            of
                true -> {ok, RscId};
                false -> {error, eacces}
            end;
        _ ->
            {error, enoent}
    end.


%% @doc Return the gravatar code of an email address. See also http://gravatar.com/
%% @spec gravatar_code(Email) -> list()
gravatar_code(Email) ->
    z_string:to_lower(z_utils:hex_encode(erlang:md5(z_string:to_lower(Email)))).


%% @doc Move all comments from one resource to another
-spec merge(m_rsc:resource(), m_rsc:resource(), z:context()) -> ok.
merge(WinnerId, LoserId, Context) ->
    z_db:q("update comment
            set rsc_id = $1
            where rsc_id = $2",
           [m_rsc:rid(WinnerId, Context), m_rsc:rid(LoserId, Context)],
           Context),
    z_depcache:flush({comment_rsc, m_rsc:rid(LoserId, Context)}, Context),
    z_depcache:flush({comment_rsc, m_rsc:rid(WinnerId, Context)}, Context),
    ok.


%% @doc Return the search as used by z_search and the search model.
search({recent_comments, []}, _OfffsetLimit, _Context) ->
    #search_sql{
        select="c.*",
        from="comment c",
        order="c.created desc",
        assoc=true
    }.
